package com.fable.sp.core.widgets;

import com.fable.sp.core.annotation.ProcessAction;
import com.fable.sp.core.annotation.ProcessEvent;
import com.fable.sp.core.annotation.RenderMode;
import com.fable.sp.core.util.HttpUtil;
import com.fable.sp.core.widget.*;
import com.google.common.collect.Maps;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.InvocableHandlerMethod;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 * .
 *
 * @author stormning on 16/8/4.
 */
abstract class GenericWidget implements Widget, EventWidget {

    private transient Map<String, Method> processActionHandlingMethodsMap = new HashMap<String, Method>();
    private transient Map<String, Method> processEventHandlingMethodsMap = new HashMap<String, Method>();
    private transient Map<String, Method> renderModeHandlingMethodsMap = new HashMap<String, Method>();
    private Map<Method, HandlerMethod> handlerMethodCache = Maps.newHashMap();

    @Autowired
    private WidgetHandlerAdapter widgetHandlerAdapter;

    public GenericWidget() {
        cacheAnnotatedMethods();
    }

    @Override
    public void processAction(ActionRequest request, ActionResponse response) throws WidgetException, IOException {
        String action = request.getParameter(ActionRequest.ACTION_NAME);
        try {
            // check if action is cached
            Method actionMethod = processActionHandlingMethodsMap.get(action);
            if (actionMethod != null) {
                invokeMethod(actionMethod, request, response);
                return;
            }
        } catch (Exception e) {
            throw new WidgetException(e);
        }

        // if no action processing method was found throw exc
        throw new WidgetException("processAction method not implemented");
    }

    private Object invokeMethod(Method method, Object... args) throws Exception {
        HandlerMethod handlerMethod = handlerMethodCache.get(method);
        return widgetHandlerAdapter.invokeWidgetHandlerMethod(HttpUtil.getCurrentRequest(), HttpUtil.getCurrentResponse(), handlerMethod, args);
    }

    @Override
    public void processEvent(EventRequest request, EventResponse response) throws WidgetException, IOException {
        String eventName = request.getEvent().getName();

        try {
            // check for exact match
            Method eventMethod = processEventHandlingMethodsMap.get(eventName);
            if (eventMethod != null) {
                invokeMethod(eventMethod, request, response);
                return;
            } else {
                // Search for the longest possible matching wildcard annotation
                int endPos = eventName.indexOf('}');
                int dotPos = eventName.lastIndexOf('.');
                while (dotPos > endPos) {
                    String wildcardLookup = eventName.substring(0, dotPos + 1);
                    eventMethod = processEventHandlingMethodsMap.get(wildcardLookup);
                    if (eventMethod != null) {
                        eventMethod.invoke(this, request, response);
                        return;
                    }
                    if (dotPos == 0) {
                        break;
                    }
                    dotPos = eventName.lastIndexOf('.', dotPos - 1);
                }
            }
        } catch (Exception e) {
            throw new WidgetException(e);
        }

        // if no event processing method was found just keep render params
        response.setRenderParameters(request);
    }

    @Override
    public void render(RenderRequest request, RenderResponse response) throws WidgetException, IOException {
        WidgetMode mode = request.getWidgetMode();
        // first look if there are methods annotated for
        // handling the rendering of this mode
        try {
            // check if mode is cached
            Method renderMethod = renderModeHandlingMethodsMap.get(mode.toString());
            if (renderMethod != null) {
                invokeMethod(renderMethod, request, response);
                return;
            }
        } catch (Exception e) {
            throw new WidgetException(e);
        }

        // if not, try the default doXYZ methods
        if (mode.equals(WidgetMode.VIEW)) {
            doView(request, response);
        } else if (mode.equals(WidgetMode.EDIT)) {
            doEdit(request, response);
        } else {
            throw new WidgetException("unknown widget mode: " + mode);
        }
    }

    private void cacheAnnotatedMethods() {
        // cache all annotated and visible public methods
        for (Method method : this.getClass().getMethods()) {
            Annotation[] annotations = method.getAnnotations();
            if (annotations != null) {
                for (Annotation annotation : annotations) {
                    Class<? extends Annotation> annotationType = annotation.annotationType();
                    if (ProcessAction.class.equals(annotationType)) {
                        String name = ((ProcessAction) annotation).name();
                        if (name != null && name.length() > 0)
                            processActionHandlingMethodsMap.put(name, method);
                    } else if (ProcessEvent.class.equals(annotationType)) {
                        String name = ((ProcessEvent) annotation).name();
                        if (name != null && name.length() > 0) {
                            processEventHandlingMethodsMap.put(name, method);
                        }
                    } else if (RenderMode.class.equals(annotationType)) {
                        String name = ((RenderMode) annotation).name();
                        if (name != null && name.length() > 0)
                            renderModeHandlingMethodsMap.put(name.toLowerCase(), method);
                    }
                    handlerMethodCache.put(method, new InvocableHandlerMethod(this, method));
                }
            }
        }
    }

    protected void doView(RenderRequest request, RenderResponse response) throws WidgetException, IOException {
        throw new WidgetException("doView method not implemented");
    }

    protected void doEdit(RenderRequest request, RenderResponse response) throws WidgetException, IOException {
        throw new WidgetException("doEdit method not implemented");
    }
}
